﻿using System;
using System.Collections.Generic;
using System.Text;
using System.Net;
using System.Net.Sockets;
using System.Threading;
using System.IO;
using System.Runtime.Serialization.Formatters.Binary;

namespace FileSharing
{
    public class ProtocolHandler
    {
        public const int MinVizinhosConnected = 2;
        public const int NodeLookupInterval = 30;
        public const int PingInterval = 15;

        List<Vizinho> VizinhosConhecidos = new List<Vizinho>();
        List<Connection> VizinhosConectados = new List<Connection>();
        List<Connection> PendingConnections = new List<Connection>();

        List<RemoteFile> SearchList = new List<RemoteFile>();

        Downloads ActiveDownloads;
        Uploads ActiveUploads;

        Mutex Conhecidos = new Mutex();
        Mutex Conectados = new Mutex();
        Mutex Pending = new Mutex();

        RoutingTable BroadcastTable = new RoutingTable();

        Diretorio LocalDirectory;

        Thread ListenThread;
        TcpListener listner;

        Thread UpdateThread;

        bool running = false;

        public enum MessageCode : uint
        {
            Hello = 1,
            HelloReply,
            Ping,
            Pong,
            NodeLookupReply,
            NodeLookup,
            Search,
            SearchReply,

            TransferHello,
            TransferHelloReply,
            RequestTransfer,
            RequestTransferReply,
            RequestPiece,
            PieceChunk,
            PieceEnd,
            TransferSucceeded,
            TransferEnd
        };

        #region Properties
        public IPEndPoint LocalAddress { get; set; }

        public String LocalName { get; set; }

        public List<Vizinho> ListaConhecidos
        {
            get
            {
                return new List<Vizinho>(VizinhosConhecidos);
            }
        }

        public List<Connection> ListaConectados
        {
            get
            {
                return new List<Connection>(VizinhosConectados);
            }
        }

        public RemoteFile[] SearchResults
        {
            get
            {
                return SearchList.ToArray();
            }
        }

        public Uploads UploadsCollection { get { return ActiveUploads; } }

        #endregion

        #region Start/Stop
        public void Start(Vizinho[] users, Diretorio dir)
        {
            // Cleanup
            PendingConnections.Clear();
            VizinhosConectados.Clear();
            VizinhosConhecidos.Clear();
            SearchList.Clear();
            //--------------------

            Settings sett = Settings.Instance();

            LocalName = sett.GetValue(Settings.Setting.UserName);

            int IncomingPort = System.Convert.ToInt32(sett.GetValue(Settings.Setting.IncomingPort));

            IPHostEntry LocalHostEntry = Dns.GetHostEntry(Dns.GetHostName());

            foreach (IPAddress IP in LocalHostEntry.AddressList)
            {
                if (IP.AddressFamily == AddressFamily.InterNetwork)
                {
                    LocalAddress = new IPEndPoint(IP, IncomingPort);
                }
            }

            // Create new connections
            /*int ConnCount = Math.Min(MinVizinhosConnected, users.Length);

            for (int i = 0; i < ConnCount; i++)
            {
                Connection NewConn = new Connection(this);

                Pending.WaitOne();
                PendingConnections.Add(NewConn);
                Pending.ReleaseMutex();

                NewConn.Connect(users[i].IP);
            }*/

            // Add known users
            foreach (Vizinho viz in users)
            {
                AddVizinho(viz);
            }

            LocalDirectory = dir;

            ActiveUploads = new Uploads(dir);
            ActiveDownloads = new Downloads(dir, LocalAddress);

            // Start Listening Thread
            listner = new TcpListener(IPAddress.Any, IncomingPort);
            listner.Start();

            running = true;

            ListenThread = new Thread(new ThreadStart(Listen));
            ListenThread.Start();

            UpdateThread = new Thread(new ThreadStart(Update));
            UpdateThread.Start();
        }

        public void Start(Diretorio dir)
        {
            Settings sett = Settings.Instance();

            LocalName = sett.GetValue(Settings.Setting.UserName);

            string DefaultAddress = sett.GetValue(Settings.Setting.DefaultAddress);
            int DefaultPort = System.Convert.ToInt32(sett.GetValue(Settings.Setting.DefaultPort));
            int IncomingPort = System.Convert.ToInt32(sett.GetValue(Settings.Setting.IncomingPort));

            IPEndPoint DefaultIP = new IPEndPoint(IPAddress.Parse(DefaultAddress), DefaultPort);
            IPHostEntry LocalHostEntry = Dns.GetHostEntry(Dns.GetHostName());

            bool findIP = false;
            foreach (IPAddress IP in LocalHostEntry.AddressList)
            {
                if (IP.Equals(DefaultIP.Address))
                {
                    findIP = true;
                }

                if (IP.AddressFamily == AddressFamily.InterNetwork)
                {
                    LocalAddress = new IPEndPoint(IP, IncomingPort);
                }
            }

            if (!(findIP && IncomingPort == DefaultPort))
            {
                Connection NoPadraoConn = new Connection(this);

                Pending.WaitOne();
                PendingConnections.Add(NoPadraoConn);
                Pending.ReleaseMutex();

                NoPadraoConn.Connect(DefaultIP);
            }

            LocalDirectory = dir;

            ActiveUploads = new Uploads(dir);
            ActiveDownloads = new Downloads(dir, LocalAddress);

            // Start Listening Thread
            listner = new TcpListener(IPAddress.Any, IncomingPort);
            listner.Start();

            running = true;

            ListenThread = new Thread(new ThreadStart(Listen));
            ListenThread.Start();

            UpdateThread = new Thread(new ThreadStart(Update));
            UpdateThread.Start();
        }

        public void Stop()
        {
            if (running)
            {
                running = false;

                listner.Stop();

                Conectados.WaitOne();

                foreach (Connection conn in VizinhosConectados)
                    conn.Close();

                VizinhosConectados.Clear();

                Conectados.ReleaseMutex();

                Pending.WaitOne();

                foreach (Connection conn in PendingConnections)
                    conn.Close();

                PendingConnections.Clear();
                Pending.ReleaseMutex();

                ActiveDownloads.Close();
                ActiveUploads.Close();
            }
        }
#endregion

        #region Threads
        // THREAD: Listen for new connections
        private void Listen()
        {
            while (running)
            {
                try
                {
                    TcpClient socket = listner.AcceptTcpClient();

                    Connection conn = new Connection(this);

                    // Add Connection
                    Pending.WaitOne();
                    PendingConnections.Add(conn);
                    Pending.ReleaseMutex();

                    conn.Start(socket);
                }
                catch (Exception e)
                {
                }
            }
        }

        // THREAD: Perform updates with neighbors
        private void Update()
        {
            while (running)
            {
                Conhecidos.WaitOne();
                Conectados.WaitOne();

                DateTime now = DateTime.Now;

                // Node Lookup
                foreach (Connection conn in VizinhosConectados)
                {
                    if (conn.ConnectVizinho.ReadyForLookup)
                    {
                        if (now.Subtract(conn.ConnectVizinho.LastNodeLookup).Seconds > NodeLookupInterval)
                        {
                            conn.ConnectVizinho.ReadyForLookup = false;

                            conn.SendMessage(new MessageNodeLookup());

                            conn.ConnectVizinho.LastNodeLookup = now;
                        }
                    }
                }

                // Ping Pong
                foreach (Vizinho viz in VizinhosConhecidos)
                {
                    if (viz.CurrentStatus == Vizinho.Status.Known || viz.CurrentStatus == Vizinho.Status.Dead)
                    {
                        if (now.Subtract(viz.LastPing).Seconds > PingInterval)
                        {
                            if (viz.ReadyForPing)
                            {
                                viz.ReadyForPing = false;
                                viz.LastPing = DateTime.Now;
                                viz.PingRequests++;

                                SendRoutedMessage(new MessagePing(LocalAddress, viz.IP), null);
                            }
                            else
                            {
                                if (viz.PingRequests > 2)
                                    viz.CurrentStatus = Vizinho.Status.Dead;
                                else
                                    viz.CurrentStatus = Vizinho.Status.Known;

                                viz.ReadyForPing = true;
                            }
                        }
                    }
                    else if (viz.CurrentStatus == Vizinho.Status.Active)
                    {
                        if (viz.ReadyForPing)
                        {
                            if (now.Subtract(viz.LastPing).Seconds > NodeLookupInterval)
                            {
                                viz.ReadyForPing = false;
                                viz.LastPing = DateTime.Now;
                                viz.PingRequests++;

                                SendRoutedMessage(new MessagePing(LocalAddress, viz.IP), null);
                            }
                        }
                        else
                        {
                            if (now.Subtract(viz.LastPing).Seconds > NodeLookupInterval)
                            {
                                if (viz.PingRequests > 2)
                                    viz.CurrentStatus = Vizinho.Status.Dead;

                                viz.ReadyForPing = true;
                            }
                        }
                    }
                }

                // Reconection Attempts
                if (VizinhosConectados.Count + PendingConnections.Count < MinVizinhosConnected)
                {
                    Pending.WaitOne();

                    while (VizinhosConectados.Count + PendingConnections.Count < MinVizinhosConnected)
                    {
                        Vizinho Possible = null;
                        foreach (Vizinho viz in VizinhosConhecidos)
                        {
                            if (viz.CurrentStatus != Vizinho.Status.Connected)
                            {
                                if (Possible == null)
                                {
                                    bool find = false;
                                    foreach (Connection conn in PendingConnections)
                                        if (conn.ConnectTo.Equals(viz.IP))
                                            find = true;

                                    if (find == false)
                                        Possible = viz;
                                }
                                else
                                {
                                    if (viz.ConnectionAttenpts < Possible.ConnectionAttenpts)
                                    {
                                        Possible = viz;
                                    }
                                }
                            }
                        }

                        if (Possible == null)
                            break;

                        Connection Conn = new Connection(this);
                        PendingConnections.Add(Conn);
                        Conn.Connect(Possible.IP);

                        Possible.ConnectionAttenpts++;
                        Possible.LastConnectionAttempt = DateTime.Now;

                    }

                    Pending.ReleaseMutex();

                    //foreach (Vizinho viz in VizinhosConhecidos)
                    //{
                    //    if (viz.CurrentStatus == Vizinho.Status.Active)
                    //    {
                    //        Connection conn = new Connection(this);

                    //        Pending.WaitOne();
                    //        PendingConnections.Add(conn);
                    //        Pending.ReleaseMutex();

                    //        conn.Connect(viz.IP);
                    //    }

                    //    if (VizinhosConectados.Count + PendingConnections.Count >= MinVizinhosConnected)
                    //        break;
                    //}
                }

                Conhecidos.ReleaseMutex();
                Conectados.ReleaseMutex();

                Thread.Sleep(500);
            }
        }
        #endregion

        #region Vizinho
        public Vizinho AddVizinho(Vizinho viz)
        {
            if (viz.IP.Equals(LocalAddress))
                return null;

            Conhecidos.WaitOne();

            foreach (Vizinho v in VizinhosConhecidos)
            {
                if (v.IP.Equals(viz.IP))
                {
                    v.Name = viz.Name;

                    Conhecidos.ReleaseMutex();
                    return v;
                }
            }

            VizinhosConhecidos.Add(viz);

            Conhecidos.ReleaseMutex();

            return viz;
        }

        public Vizinho GetVizinho(IPEndPoint ip)
        {
            foreach (Vizinho v in VizinhosConhecidos)
            {
                if (v.IP.Equals(ip))
                    return v;
            }

            return null;
        }

        public void VizinhoConnect(Connection conn)
        {
            Pending.WaitOne();
            PendingConnections.Remove(conn);
            Pending.ReleaseMutex();

            Conectados.WaitOne();
            VizinhosConectados.Add(conn);
            Conectados.ReleaseMutex();
        }

        public void VizinhoRejected(Connection conn)
        {
            Pending.WaitOne();
            PendingConnections.Remove(conn);
            Pending.ReleaseMutex();
        }

        public void VizinhoDead(Connection conn)
        {
            Conectados.WaitOne();

            if (conn.ConnectVizinho.CurrentStatus != Vizinho.Status.Dead)
                conn.ConnectVizinho.CurrentStatus = Vizinho.Status.Known;

            VizinhosConectados.Remove(conn);
            Conectados.ReleaseMutex();

            conn.Close();

            Log.Instance().PrintLine("Neighbor " + conn.ConnectVizinho.IP + " disconnected");
        }

        public void VizinhoActive(IPEndPoint vizIP)
        {
            Conhecidos.WaitOne();

            foreach (Vizinho viz in VizinhosConhecidos)
            {
                if (viz.IP.Equals(vizIP))
                {
                    if (viz.CurrentStatus != Vizinho.Status.Connected)
                    {
                        viz.CurrentStatus = Vizinho.Status.Active;

                        Log.Instance().PrintLine("Neighbor " + viz.IP + " (" + viz.Name + ") is active");
                    }

                    viz.PingRequests = 0;
                    viz.ReadyForPing = true;
                    break;
                }
            }

            Conhecidos.ReleaseMutex();
        }
        #endregion

        #region Messages
        public void SendRoutedMessage(RoutedMessage mens, Vizinho lasthoop)
        {
            // Check if the message already passed by here.
            //if (BroadcastTable.CheckMessage(mens))
            //    return;

            //BroadcastTable.AddMessage(mens);

            bool found = false;

            mens.TTL--;
            mens.HoopsTaken++;

            if (mens.TTL == 0)
                return;

            Conectados.WaitOne();

            foreach (Connection conn in VizinhosConectados)
            {
                if (conn.ConnectVizinho.IP.Equals(mens.To))
                {
                    conn.SendMessage(mens);

                    found = true;
                    break;
                }
            }

            if (!found)
            {
                foreach (Connection conn in VizinhosConectados)
                {
                    if (lasthoop != null)
                    {
                        if (!conn.ConnectVizinho.IP.Equals(lasthoop.IP))
                            conn.SendMessage(mens);
                    }
                    else
                        conn.SendMessage(mens);
                }
            }

            Conectados.ReleaseMutex();
        }

        public void SendBroadcastMessage(BroadcastMessage mens, Vizinho lasthoop)
        {
            // Check if the message already passed by here.
            //if (BroadcastTable.CheckMessage(mens))
            //    return;

            //BroadcastTable.AddMessage(mens);

            mens.TTL--;
            mens.HoopsTaken++;

            if (mens.TTL == 0)
                return;

            Conectados.WaitOne();

            foreach (Connection conn in VizinhosConectados)
            {
                if (lasthoop != null)
                {
                    if (!conn.ConnectVizinho.IP.Equals(lasthoop.IP))
                        conn.SendMessage(mens);
                }
                else
                    conn.SendMessage(mens);
            }

            Conectados.ReleaseMutex();
        }

        public void AddMessage(BroadcastMessage mens)
        {
            BroadcastTable.AddMessage(mens);
        }

        public bool CheckMessage(BroadcastMessage mens)
        {
            return BroadcastTable.CheckMessage(mens);
        }

        #endregion

        #region Search
        public void SendSearch(String query)
        {
            SendBroadcastMessage(new MessageSearch(LocalAddress, query), null);
        }

        public void ClearSearchResults()
        {
            SearchList.Clear();
        }

        public LocalFile[] Search(String query)
        {
            return LocalDirectory.FindFile(query);
        }

        public void SearchMatchReply(RemoteFile[] matches)
        {
            foreach (RemoteFile file in matches)
                SearchList.Add(file);
        }
        #endregion

        #region Transfer
        public void NewUploadTask(UploadTask upload)
        {
            ActiveUploads.AddUploadTask(upload);
        }

        public void StartDownload(RemoteFile[] sources)
        {
            DownloadTask task = new DownloadTask(sources, ActiveDownloads);
            
            ActiveDownloads.AddTask(task);

            task.Start();
        }

        public TransfersCollection GetTransfers()
        {
            TransfersCollection col = new TransfersCollection();

            col.DownloadCollection = ActiveDownloads.GetDownloadCollection();
            col.UploadCollection = ActiveUploads.GetUploadCollection();

            return col;
        }
        #endregion
    }

    public class RoutingTable
    {
        private class MsgViz
        {
            public Guid IDMsg;
            public IPEndPoint IDViz;
        };

        List<MsgViz> MsgList = new List<MsgViz>();

        Mutex ListMut = new Mutex();

        /// <summary>
        /// 
        /// </summary>
        /// <param name="msg"></param>
        public void AddMessage(BroadcastMessage msg)
        {
            MsgViz CurrentMessage = new MsgViz();
            CurrentMessage.IDMsg = msg.ID;
            CurrentMessage.IDViz = msg.From;

            ListMut.WaitOne();
            MsgList.Add(CurrentMessage);
            ListMut.ReleaseMutex();
        }

        /// <summary>
        /// 
        /// </summary>
        /// <param name="msg"></param>
        /// <returns>
        /// Retorna "true" se a mensagem já passou pelo nó.
        /// Reorna "false" caso contrário.
        /// </returns>
        public bool CheckMessage(BroadcastMessage msg)
        {
            MsgViz TestMessage = new MsgViz();
            TestMessage.IDMsg = msg.ID;
            TestMessage.IDViz = msg.From;
            bool contain = false;

            ListMut.WaitOne();

            foreach (MsgViz element in MsgList)
            {
                if (element.IDMsg.Equals(msg.ID) &&
                    element.IDViz.Equals(msg.From))
                    contain = true;
            }

            ListMut.ReleaseMutex();

            if (contain)
                return true;
            else
                return false;
        }
    }

}
